Skip to content

Mac-first completion: platform crate, AArch64 tiers, emit and runtime conformance, review fixes - #229

Open
rabbitson87 wants to merge 334 commits into
mainfrom
completion/mac-first
Open

rabbitson87 wants to merge 334 commits into
mainfrom
completion/mac-first

Conversation

@rabbitson87

@rabbitson87 rabbitson87 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

What this is

Two stretches of the completion plan, done on an Apple M3 Max (aarch64-apple-darwin), plus a review round. The first made the workspace build and pass on macOS and added local CI and sweep tools. The second makes emitted JavaScript and the runtime behave correctly. The review round fixes what two reviewers and an independent read-only audit found in the second. 291 commits against main. The hosted workflows are unchanged.

This is not the finished product. The compiler and package roots are still open, and nothing here claims otherwise.

Please merge without squashing. AGENTS.md asks for a bisectable series; see Process notes for the commits that fall short of it.

Results at 4154686

Measure Start of the second stretch Now
test262 (in-process runner, files) 40,563 of 51,946 47,240, identical failing set on interpreter, JIT and AOT
test262 interpreter receipt (applicable rows) not taken 47,111 of 51,830 at eb2eec1; no newer receipt
TypeScript 7.0.2, shard 0/64 223 of 1,044 271, zero regressions
TypeScript 7.0.2, full receipt 14,894 of 66,786 at 4db845c not retaken
just ci pass at eb2eec1 pass at 4154686

The two test262 numbers use different denominators: the receipt counts the catalog's applicable rows, the runner counts files. No JIT, AOT or full TypeScript receipt has been taken at this commit; that is the full-baseline card and it stays open.

Hosted CI at 4154686: Quality, Corpus, Formal, Preflight, Windows native boundary and CodeQL pass (Quality fails on main). The four Conformance receipt shard jobs and Conformance receipt accounting fail: each shard runs into its 45-minute job timeout (.github/workflows/ci.yml:277) and is cancelled, and accounting fails because no shard delivered. main's latest run (4ba4408) fails the same way, so this predates the branch. The workflow is unchanged here, so the timeout stays.

What changed in the second stretch

Area Effect
JavaScript emit Namespaces, enums, nested expressions, import aliases and exported var/let lower correctly.
Lowering Array holes, object literal define semantics, native construct, super through a home object, read-write keys converted once, for-of targets.
Runtime library Intl, Temporal, Atomics, Iterator helpers and the RegExp protocol now run. Most of that code was uncompiled or missing.
Garbage collection Weak holders are found during marking: a 505,000-object loop went from over 120 s to 6.65 s.
Verification test262 host sets TZ=UTC; .types/.symbols headers use the catalog path; target cells are format-neutral across seven targets.

What changed in the review round

Area Effect
Tooling recount now reruns a sample of passing rows (default 500) so --fail-on-regression can fire; doctor checks each tool alone; perf harnesses read only the host facts they use; the status report refuses its own sidecar's path.
Linker The in-process GOT is 8-aligned and keeps a relocation's addend.
GC safety A builtin's Rust locals are rooted until it returns, so toSorted(cmp), Object.groupBy, f(...gen()), promise.then and many others no longer die with an uncatchable heap slot does not exist. Loops that use each callback result once (reduce, forEach, map and their like) keep only the latest 32.
Module output Every ES and Node module kind emits (es2020, es2022, preserve, node16/nodenext, per file by extension and package.json); CommonJS imports stay in source order; function exports hoist above the requires; re-exported imports are live getters; exported locals stay current on every write; export = is module.exports; string export names are quoted from their decoded text.
Import elision An import is kept if the file reads it as a value, in any position. What another file erases is decided from its exports, through barrels and export *, and only for export { x } and export default x. verbatimModuleSyntax, JavaScript files, @jsx, emitDecoratorMetadata and reactNamespace keep every import.
Language semantics A class called without new throws; static name defines instead of throwing; field initializers resolve in the class's scope; an arrow's super() shares the constructor's state; a destructuring target is evaluated before its value; a this parameter takes no argument.
Other Math.abs(new Number(-1)), 'ab'.repeat(0), catch-pattern defaults, an empty ES module keeping export {};, and a lost reply when shutdown raced a request in the API server.

Verified

  • just ci passed at 4154686; catalog, completion and diagnostics regenerate --check pass.
  • No new #[ignore], todo!, unimplemented! or #[allow] across the second stretch and the review round. .github, lockfiles (second stretch onward), rust-toolchain.toml and every generated file are unchanged by this range, except verification/manifest.lock.json, which 1352e7b regenerated for the target cell change.
  • Regression tests run on all three tiers for the new runtime and lowering behavior; the GC test fails with heap slot ... does not exist when its root is removed.
  • Each emit fix was compared with tsc 7.0.2 output or Node.

Not verified

  • Windows runtime. Only compilation is proven. driver.rs still calls cc ... -o, and the platform crate has no Windows implementation at all: its machine-fact, process-region and peak-memory readers return Unsupported there (there is no windows.rs; the fallbacks are in crates/bamts-platform/src/lib.rs). Expect failures; they are the input for the Windows tasks.
  • Linux and x86_64 macOS at run time. Compile-checked only. The x86_64 macOS archive in this series is cross-built and carries an empty runtime archive.
  • Auto-merged work/stdlib code. The seven conflicted files were reviewed, and a read-only audit covered the twelve files both sides touched. The rest of the merge was not audited.
  • Review is by reading and by running small programs. Nothing is proven complete.

Known defects and limits

Known test262 regressions against eda5413 (11 files, down from 12): language/arguments-object/10.6-13-a-{2,3}.js, language/eval-code/{direct,indirect}/non-definable-global-{function,generator}.js, built-ins/ThrowTypeError/unique-per-realm-non-simple.js, built-ins/Date/prototype/setFullYear/15.9.5.40_1.js, and three Temporal toLocaleString/ignore-timezone.js files.

Open and not fixed: f.length counts default parameters; module: preserve with export =; .mts import x = require(); moduleDetection: force and import.meta miss the export {} marker; CommonJS source maps skip require lines and newLine: crlf leaves the prelude LF; a trailing comment after an import is lost; __importDefault/__importStar are not emitted; array destructuring never closes iterators; class A {} interface A {} and <NS.Comp /> raise false errors; @jsx pragmas are ignored by the checker; shebangs are dropped; CommonJS import() is not rewritten; class extends null throws at definition; a builtin that collects very large callback results keeps them all rooted until it returns, so it can reach the heap limit where Node would not (String.prototype.replace with a huge result is one).

Product limits: local-time Date throws unless TZ is UTC or a POSIX form, which includes the normal unset case; locale data is en/en-US only and chinese, dangi and islamic calendars are missing; bamts file.ts writes a native executable and no .js, while a tsconfig.json project writes JavaScript; arguments is still an Array; direct eval cannot see caller locals; Annex B block functions are missing.

Decisions to review

Test expectations changed. The plan's rule is to never change an expected value to make something pass. These changed, with reasons, and still need review:

  • Behavior changed on purpose: 7d321c4, f13e726, 494c9fb, a8a641c (atomics_claims_no_methods, changed by a worker without prior approval), 2a6cb50, f8cdfaa.
  • Test judged wrong against test262, its own assertions or tsc: 0183c1f, 65e026b, 5c09826, 5861c67, cacb3d7, e0521c6, 190379b, 1b67908, and commonjs_export_specifiers_read_what_the_name_became (this series had added that expectation in d086b56; tsc 7.0.2 emits the getter).
  • Deleted: seven option-reader tests in fe8e661, removed with the dead code they covered.
  • host_conditions_come_from_the_platform_crate gained a branch for a Linux host with no cpufreq; its assertions are unchanged.

Held, with tests unchanged. Each pinned test conflicts with test262 or tsc, so the old behavior stays until someone decides:

  • date_parse_extended_years_round_trip (blocks the japanese calendar, 144 files, ready on a local branch)
  • commonjs_class_and_variable_exports_receive_void_0_preamble
  • the GC schedule tests
  • namespace_reports_unlowered_diagnostic_in_javascript
  • iterator_rejects_non_array_receiver
  • three __defineGetter__ and __defineSetter__ tests in annex_b.rs
  • runtime_invalid_date_propagates_and_iso_throws
  • locale_compare_uses_observable_utf16_lexical_signs
  • promise_all_closes_an_abrupt_iterator_and_keeps_the_original_throw
  • installed_methods_have_standard_descriptors_names_and_lengths
  • the own stack accessor tests
  • instance fields use [[Set]], so class B extends A { x = 1 } runs an inherited setter (Node defines); pinned by instance_field_init_after_super
  • decorated classes and decorated static fields keep assigning; pinned by two lower tests
  • export {}; is not added when module is unset; pinned by two emitter tests
  • BAMTS-C026 on an arrow's super(); tsc accepts it
  • a second direct super() refuses before re-running the parent

Review comments not changed, with reasons: the tests without a Linux gate (jit.rs region tests, run_with_telemetry_reports_peak_rss) stay ungated because gating them would be the hiding #[cfg] the plan forbids, they fail on Windows only because the platform readers are unimplemented there, and that failure is the input for the Windows task; the contract leaves that own files not yet written (windows_link.rs and two toolchain records) are real deliverables of open cards, and their ledgers now say which card writes each; no CI job runs ci-npm-release because it needs all five host archives and hosted workflows are deliberately unchanged, so five-host-install runs it by hand.

Process notes

  • af1a13c, 3dda270 and 3cfa106 each hold more than one mechanism, and 49da830 holds two that share their helpers.
  • In the work/stdlib merge, cargo check --all-targets passes on the first 75 of 76 commits, and per-commit unit tests ran only on the first 48 plus six others, so git bisect through that range may be less reliable than elsewhere.
  • Windows-only unused-import warnings in driver.rs and filesystem.rs tests predate this work.
First stretch, as written at 1948470

What this is

The first stretch of the completion plan, done on an Apple M3 Max (aarch64-apple-darwin): make the workspace build and pass there, move OS reads behind one crate, make in-process AOT/JIT work on AArch64, add local CI recipes, and add the tools that turn a conformance sweep into a short list of fixable causes. 55 commits, 57 files. The hosted workflows are unchanged.

Please merge without squashing. AGENTS.md asks for a bisectable series with one mechanism per commit, and the commits are built that way.

What changed

Area Commits (first to last) Effect
Platform boundary b372182 … e885f01, 5614d90 … 232c221, 5ff64cd New bamts-platform crate. Machine facts, CPU facts, process regions and peak memory are read there for Linux (x86_64 and AArch64) and macOS. Nothing outside it reads /proc or /sys. portability-check enforces that.
AArch64 AOT and JIT 883387d, b27b14c, ab6d3dd, f5655e2, 343433f In-process AOT picks its target from the host ABI, links AArch64 GOT relocations, accepts CALL26 as the reader reports it, and flushes the instruction cache. JIT code is checked against the host process map on every host.
Windows build 7fefcbc, b494022 The workspace and its tests now compile for x86_64-pc-windows-msvc. This is a compile result only.
Test isolation 7ecdff8, ee90d06, 7fdeb9b, d96f07a, 7d9951b, 9b4def8, f2d1342 Fixtures get their own scratch directories and derive their platform from the host, so the suite no longer depends on test order or on being on Linux.
Local CI c7bfc36, e92e173, 9f3dc78 just ci runs fmt, check, clippy, workspace tests, npm tests, the corpus check, cross-target cargo check and the portability check on one machine. ci-npm skips the two groups that need all five host archives; ci-npm-release runs everything.
Contract e9450c8 Nine leaves added (105 to 114): B7.2, E2.4, E3.4 to E3.7, E4.6 to E4.8, so the five-host execution scope, including Windows native output and its target cell, is in the root contract. FROZEN_LEAF_COUNT is now 114, with a test. The nine ledgers are in the same commit because validation rejects either half alone.
Sweep tooling 710e645, 4eb2d11, 5fd123c, 42254fc, b42c264, af4b98e, 4a95f44, 3c331d3, 1948470 suite families, suite diff, suite recount, suite status, the matching just recipes, just gate-status, and structural-mismatch messages that name the first differing line. Receipt states are unchanged: a shard rerun before and after the message change gave identical states on all 1,044 rows.
Other a0310fc, 8c592e6, 91b0b04 Removes npm/test/bamti-native-runner.test.mjs, which tested functions 275e12a had already removed; adds the single-worker change-window procedure; ignores the local plan ledger.

Verified

On macOS arm64 at 1948470, just ci printed LOCAL CI PASS:

  • cargo fmt --all --check, cargo check --workspace --locked and cargo clippy --workspace --all-targets --locked -- -D warnings pass.
  • cargo test --workspace --locked: 4,714 passed, 0 failed, 12 ignored.
  • npm: 86 tests, 82 pass, 0 fail, 4 skipped by ci-npm.
  • just xcheck compiles for x86_64 macOS, x86_64 Linux, AArch64 Linux and x86_64 Windows. portability-check passes.
  • The new report code was mutation-tested (18 mutants in the report module alone): each mutant had to make a test fail, and the file was restored byte for byte afterwards.
  • Four real conformance receipts were produced on this Mac (compiler, interpreter, JIT, AOT lanes).

Not verified

  • Windows runtime. Only compilation is proven. driver.rs still calls cc ... -o, which is not an MSVC command line, and the platform crate has no Windows implementation (its readers return Unsupported). Expect Windows test failures. Those failures are the input for the next Windows task.
  • Linux and x86_64 macOS at run time. The Linux targets are compile-checked only. The x86_64-apple-darwin npm archive in this series was cross-built from arm64, so it carries an empty runtime archive; it passes the install-metadata tests and nothing else.
  • ledger rebuild --check rejects all 248,828 obligations. It fails the same way on 9f3dc78, the commit before the contract change, so this PR does not cause it. It stays failing until the measurement baseline is restored.
  • completion regenerate --check and the catalog check pass.

Decisions to review

  • Deleting bamti-native-runner.test.mjs is reversible with git revert a0310fc.
  • The nine contract leaves were added to verification/completion-program.toml by hand, because completion regenerate validates and rewrites that file but does not derive leaves. After the edit it wrote back identical bytes. The reasoning is in e9450c8.
  • A failure "family" is <facet>/<slug> of the diagnostic text. It is defined in one function, family_name in sweep_report.rs.

Known leftovers

  • The types and symbols facets still fall into two large text families (174 and 168 rows) because their record lines have no line kinds yet.
  • 19 JavaScript rows are not routed to the javascript observable for project cases, and 10 rows fail with "cannot resolve ". Both are compiler work for later.
  • api_server::tests::shutdown_answers_then_stops_the_loop failed once in earlier runs and did not recur in three full runs. It is still unexplained.
  • Three #[ignore]d tests are unchanged.

@gormes-agent

gormes-agent Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Review of 1948470: changes needed before merge

I found two real defects and one stale gate ledger. The AArch64 linker code is correct.

Must fix

1. just recount cannot report a regression (high)

  • crates/bamts-verification/src/suite/completion.rs:884-887, sweep_report.rs:91, sweep_report.rs:453.
  • recount_suite keeps only rows that have a family(). A row has a family only if its state blocks completion (blocks_completion). Thus every rerun row was non-PASS in the baseline.
  • render_recount returns diff.regressions().len(), which counts only PASS to non-PASS moves. That count is always 0, so --fail-on-regression never fires.
  • The Justfile comment says "A fix must convert a row and regress none." The command cannot check this. A fix that breaks passing rows outside the family still exits 0.
  • The unit test at sweep_report.rs:1031 puts a PASS row in before. The real code path never makes such a row, so the test does not catch this.

2. just doctor does not see missing tools (medium)

  • Justfile:10: command -v ld.lld llvm-readelf lake.
  • With many names, command -v returns the status of the last name only. bash -euo pipefail -c 'command -v nosuchA ls' exits 0.
  • If lake is present, the recipe prints DOCTOR PASS even when ld.lld or llvm-readelf is missing.
  • Fix: one command -v line for each tool.

3. The root-native.md G3 ledger is stale (medium)

  • crates/bamts-verification/src/completion.rs:274 now prints targets=49/49.
  • .outline/gates/root-native.md:17 still expects targets=28/28, so G3 can never pass as written.
  • The code does not calculate 49. It is a hardcoded literal, and verification/toolchains/ has only 4 cells. Explain where 49 comes from, or calculate it.

Should fix

4. perf_jit and perf_stage0 now fail on more hosts (medium)

  • crates/bamts-verification/src/perf_jit.rs:105-111, perf_stage0.rs:45-52.
  • Before, these read only swap and affinity. Now they call all of machine_facts() and thread_placement(), which use get_mempolicy(2) and fully parse /proc/version and /proc/cpuinfo.
  • Reproduced: under Docker's default seccomp, get_mempolicy returns EPERM and the capture fails. x86 VMs with no microcode line (linux.rs:69) and ARM hosts with physical_package_id = -1 (linux.rs:244-256) can also fail now.
  • The new tests conditions_come_from_the_platform_crate and host_conditions_come_from_the_platform_crate unwrap these calls, so they fail on those hosts too.
  • Fix: read only the values these two need.

Low

  • GOT base can be misaligned (crates/bamts-codegen/src/aot/in_process_link.rs:291-296, 365). data_end is not aligned after the last data section. If that section's size is not a multiple of 8, patch_ldr_got_lo12 rejects the link with RelocationMisaligned. No corruption, but it depends on section order. section.align() is also ignored.
  • GOT relocation addend is dropped (ADR_GOT_PAGE / LD64_GOT_LO12_NC arms). Cranelift emits A=0 today. Reject A≠0 so a wrong address fails loudly.
  • Tests lost their Linux gate. cfg(target_os = "linux") was removed from the JIT region tests in jit.rs, run_with_telemetry_reports_peak_rss in bamts-runtime, and tests in perf.rs, perf_jit.rs, perf_stage0.rs. On Windows the platform crate returns Unsupported, so they fail there instead of being skipped. Gate them with cfg(any(target_os = "linux", target_os = "macos")).
  • Stage0 governor (linux.rs:422-424 via perf_stage0.rs:48) now records a missing governor as "unknown" instead of failing as before.
  • Five new contract leaves own files that do not exist yet (historical.rs, windows_link.rs, three toolchain JSON files). verify_ownership emits OrphanOwnership for them. This may be on purpose.
  • Nothing runs ci-npm-release. No workflow or recipe calls it, so the 7 install tests that ci-npm skips never run.
  • --out sidecar (sweep_report.rs:688): if --out ends in .json, the sidecar path equals the report path and the JSON overwrites the Markdown.

Checked and correct

  • AArch64 encoders: ADRP page math and 21-bit range, LDR lo12 scaling and alignment, CALL26 range. icache flush and RW→RX order are correct. On linux-arm64, 14/14 in_process_link tests pass and exercise real relocations 311 and 312.
  • macOS FFI struct sizes (96 and 1272 bytes) match proc_info.h. Peak memory units are unchanged on Linux.
  • FROZEN_LEAF_COUNT = 114 matches the toml and the authority table.
  • facets.rs changes only message text; pass/fail logic is unchanged.
  • Test scratch directories cannot collide.
  • Deleting bamti-native-runner.test.mjs is correct: it imported functions removed in 275e12a, so it could not load on main.
  • Test runs: bamts-platform 31/31, verification subset 76/76.

Not tested

  • macOS runtime, Windows runtime, just xcheck.
  • Full bamts-verification test suite (build ran past the time limit).

Top-level `var` and function names lived in module cells, so
`globalThis.x` could not see them and `asyncHelpers.js`, which looks for
`$DONE` on the global object, could not run. Whether a file is a Script cannot
be read from the file: Node runs the same bytes as a CommonJS module, where
those names must stay local.

So the host says. `compile_script_file` sets `LowerOptions::script_entry` for
the entry file, which keeps its `var` and function names out of module cells,
hoists each `var` onto the global object without disturbing one that exists,
and binds `this` to the global object even in strict code. `let`, `const` and
`class` stay off the global object. Knowing the goal also makes `import`,
`export` and `import.meta` in a Script real early errors, which could not be
decided from the tree. The test262 lane uses this for every non-module case;
nothing else changes.
`let {} = undefined` did nothing, because only reading a property could fail
and an empty pattern reads none. With a computed key, the key also ran before
the failure. Binding and assignment patterns now require an object-coercible
value before anything else, as the specification does.
A generator ran nothing until its first `next`, so a default that throws, or a
pattern given undefined, failed later than the call that caused it, or not at
all when the generator was never advanced. The specification binds parameters
during the call.

The compiler now ends the prologue of a generator with non-simple parameters
with a `Suspend` and sets `binds_at_call`, so a generator whose parameters
cannot run code stays byte-identical. A call runs such a generator to that
point. Its first `next` then resumes the suspended point, and `return` and
`throw` arrive before any `try` in the body has started, as they would at the
start. An async generator stops there without touching its empty request
queue.

The bytecode size pin and the decoder's unknown-bit case move with the flag.
The parser reads any `import.x` as a meta property, as `tsc` does, so
`import.UNKNOWN('a')` ran and `new import.defer('a')` was accepted. The early
pass now reads the word: `import.defer` and `import.source` exist only as the
callee of a call with a specifier and options, and nothing else but `meta`
exists. `new.target` and `import.meta` cannot contain escapes, and `new` cannot
reach `import()` through a member chain.
The parser built an await expression wherever `await` could start one, so
`await(null)` in a plain function awaited instead of calling, and
`await instanceof f` failed to parse. TypeScript keeps that reading so it can
report misuse. In JavaScript, `await` outside an async function is now a name.
At the top level of a file the goal is unsettled, so module syntax decides:
without any, `await` stays a name unless what follows could not continue an
expression, which is how Node decides the same file.
These all parsed and ran, and each is one small static rule: reserved words
spelled with escapes, legacy octal in strict code and in templates, shorthand
that only a pattern allows, a comma after a rest element, `const` and `using`
without a value, `using` in patterns and case clauses, `return` outside a
function, mixing `??` with `&&` or `||`, a template after `?.`, logical
assignment to a call, a parenthesized pattern, a label on a function in an
`if`, a private name not followed by `in`, `await` where only a name can be,
and `await` or `yield` naming a function that binds them.

Across every test262 variant this rejects 183 more invalid files and no valid
one.
The loader stopped the whole program at the first import it could not resolve,
so a check run saw one error at most, and 3,386 TypeScript conformance rows
never reached the checker. `tsc` reports TS2307 for each one and keeps going.

`ProgramLoader::tolerating_unresolved_imports` does that for a run that only
checks. The loader's rules are the runtime's, so a package the runtime refuses
still gets `tsc`'s own resolver, which reads `types` and `main` where the
runtime needs `exports`. What neither finds becomes an edge the frontend
reports, unless a `declare module` satisfies it. A run that executes keeps
failing at load, because it cannot start with a module missing.
`eval` was absent, which failed every test or program that named it. With a
host script compiler it now runs a string as a classic script against the
global object and returns its completion value, and returns anything else
unchanged. It shares its compile path with `Function`. A host with no script
compiler still has no `eval`, so the name reads as undeclared there rather than
failing when called.

A classic script now also hoists its `var` names and binds `this` the way an
entry Script does, because `eval` and `vm` both need that.

Every call behaves as an indirect one for now: `eval` cannot see the locals of
its caller, which needs scope access that lowering does not provide yet. Such
code fails with a ReferenceError, so nothing silently reads the wrong binding
unless a global has the same name.
`$262` did not exist, so every test that detached a buffer or read the host's
global object failed for a reason that had nothing to do with the engine.

The lane now defines it before the harness, with the attributes
`INTERPRETING.md` requires. `detachArrayBuffer` goes through a real
`ArrayBuffer.prototype.transfer`, and `evalScript` through real `eval`. Each
captures what it uses, because tests replace builtins. `createRealm` and
`agent` stay absent: nothing here can back them yet, and a test of a missing
facility must fail rather than pass against an inert hook.
`String.prototype.toString` and `valueOf` and `Boolean.prototype.toString` were
never installed, so `'a'.toString()` found `Object.prototype.toString` and
returned `[object String]`. All three now return the receiver's own primitive
and refuse any other receiver.
Big::set_bit resized its limb vector to word + 1 unconditionally. div_rem
sets quotient bits from the top down, so each lower bit truncated the
limbs already written, and every quotient of 2^32 or more lost its high
limbs.

Number.prototype.toPrecision then never returned for ordinary values at
precision of about 15 and above: rounded_significand saw too few digits,
lowered the exponent, saw too many, and oscillated forever. toFixed and
toExponential reach the same division through exact_round_scaled.

The loop sits inside a builtin, so no deadline in the interpreter loop
can interrupt it. One test262 file that calls toPrecision(16) was enough
to hang a whole directory run.

The regression test's expected digits were computed with Python's
decimal module, independently of this code.
A substitution was joined with `+`, which applies `ToPrimitive` with the
default hint, so `${o}` called `valueOf` before `toString`. It is also why
`${x}` threw for every Temporal object, whose `valueOf` must throw. The new
`UnaryOp::ToString` applies the conversion the specification names before the
join, so a Symbol throws too.
Code reaching the compiler at runtime only got the parser's checks, so
`eval('"use strict"; with (a) {}')` compiled and `Function` accepted strict-mode
violations. A classic script now goes through the same pass as a file.
Function name inference did not exist: `var f = function () {}` left `f.name`
empty, and a named function expression lost even its own name. Anonymous
functions, arrows and classes now take the name of the variable, assignment
target, default's binding, literal object key or static field they directly
initialize. Parentheses are transparent, and `(0, function () {})` and
`(x) = function () {}` stay anonymous, as the specification requires. A class
keeps a static `name` member of its own.

`[x = f()] = []` also bound through the element's whole pattern, so a default
that produced undefined ran twice. It now binds the target.

`named_class_constructor_carries_source_name_metadata` asserted that
`const C = class {}` stays unnamed, which `language/statements/const/
fn-name-class.js` contradicts. That assertion now covers `(0, class {})`, which
keeps its original intent, and a new one covers the binding. Computed keys and
instance fields are not named yet.
`builtins/date.rs` and `builtins/json.rs` were restored by a recovery commit
but no module declares them: `builtins/mod.rs` compiles `date_full.rs` and
`json_edge.rs`. Remove both and repoint the two `bigint.rs` doc comments that
named `json.rs` at the module that does the work.
Reconnect `builtins/atomics.rs` to the module tree, reduced for now to the
SharedArrayBuffer half: the constructor, `byteLength`, `maxByteLength`,
`growable`, `grow`, `slice`, and species. TypedArray and DataView already view
`SharedBlock` through `ViewBuffer`, so they gain shared buffers unchanged.

`grow` charges the delta through the existing helper. `slice` shares the species
lookup with ArrayBuffer. A non-object `options` argument means a fixed-length
buffer, and a failed allocation is a RangeError rather than a fatal error.

The `SharedArrayBuffer waits for atomics install` assertion pinned the state
this replaces, so it now asserts the global exists.
`Machine::is_object` excluded every primitive heap entry except Symbol and
PrivateName, so a Symbol passed its call sites as an object. Reflect and Proxy
accepted Symbols where they must throw, `Object(symbol)` did not box its
argument, `JSON.stringify` serialized a Symbol it must skip, the "can be held
weakly" branch in WeakRef and FinalizationRegistry was unreachable, and
`built-ins/Symbol` and `built-ins/Map` hung outright.

Measured per file over Symbol, WeakRef, WeakMap, WeakSet, FinalizationRegistry,
Reflect, Proxy, Promise, Map, Set, JSON, Function, ArrayBuffer, DataView,
SharedArrayBuffer, and Object: 34 fixed, none regressed. Symbol and Map hung
before the change, so those two directories have no per-file comparison.
Replace the empty `Atomics` namespace with `add`, `and`, `compareExchange`,
`exchange`, `isLockFree`, `load`, `or`, `pause`, `store`, `sub`, and `xor`.

Each operation follows the specification's order: validate the view, coerce the
index against the bounds taken before coercion, coerce the operands, then
revalidate, because a coercion can run guest code that detaches or shrinks the
buffer. `store` returns the coerced operand rather than the wrapped element.
Writes to a shared block run under the block's lock, which every other access
takes too, so a read-modify-write is indivisible.

`integer_or_infinity` now returns +0 rather than -0 for a small negative
fraction, as ToIntegerOrInfinity is defined to.

`atomics_claims_no_methods` pinned the empty namespace this replaces and cannot
hold once Atomics has methods, so it now pins the exact method set instead.
The well-known symbol was never installed, so `{ [Symbol.toPrimitive]() {} }`
created a property named "undefined" and every coercion ignored the method. In
`Atomics.wait` that turned a timeout of `false` into NaN, which is by definition
an unbounded wait, and the test blocked forever.

Install the symbol and give ToPrimitive its first step: read the method, call it
with the "string", "number", or "default" hint, and reject an object result or a
non-callable method. `null` and `undefined` fall through to the ordinary methods.

`Date.prototype[Symbol.toPrimitive]` and `Symbol.prototype[Symbol.toPrimitive]`
are not added; `Date` keeps its existing default-hint handling.

Measured per file over Symbol, Date, Math, BigInt, String, JSON, and the
addition, equality, relational, template, and computed-name language tests:
43 fixed, none regressed.
`+object` went through the non-observable number conversion, so it never called
`valueOf` or `Symbol.toPrimitive` and yielded NaN. Use the observable ToNumber,
which also makes `+proxy` and `+object` with a throwing method behave.

`Number(object)`, `-object`, and `~object` share the same fault and are not
changed here.
Give each shared block a real WaiterList: a FIFO queue per byte position, guarded
by its own lock. `wait` compares the value and enqueues inside that critical
section, so a writer that stores and then notifies either changes what the
comparison sees or finds the waiter queued. It then suspends the agent on a
condition variable until woken or its timeout passes, returning "not-equal",
"ok", or "timed-out". A timeout that finds its waiter already taken lost a race
to a notifier, and reports "ok".

There is one agent, so nothing can notify a suspended `wait` yet; the unit tests
stand a thread in for the second agent to prove the wake path. A wait with no
timeout therefore blocks forever, as the specification requires, and only the
machine's cancellation token ends it.

`Limits::agent_can_suspend` is the agent's [[CanBlock]] and defaults to true.
`notify` on unshared memory validates its arguments and returns 0.
`waitAsync` queues a real waiter and returns a pending promise. `notify` resolves
it on the spot, which is what NotifyWaiter does for a waiter of the current agent,
so its reactions run at the next microtask checkpoint. A finite timeout arms a
host timer that settles the promise with "timed-out"; `notify` disarms it. A
timeout of zero, or a value that does not match, answers without a promise.

The block's list holds only an id. The machine owns the record with the promise
and traces it, because a Value inside a block would be invisible to the collector.
An untimed wait arms nothing, so it does not keep the event loop alive: with one
agent nothing can ever notify it, and the program ends with the promise pending.

`TimerProvider` delays are `u32`, so a longer timeout re-arms until all of it has
passed rather than firing early. `clearTimeout` ignores a bare number that names
one of these timers, since a script has no business cancelling them. On a host
without timers a timed wait fails with `TimerProviderFailure`; an untimed one
works.
`ExternalInbox` was built for a `waitAsync` that settled from a helper thread, and
nothing ever produced a job for it. NotifyWaiter resolves a same-agent waiter
directly and timeouts run on host timers, so only a second agent could use it, and
there is none. Remove the queue, its drain, and the two `allow(dead_code)`s that
hid it. An agent implementation can add a queue shaped by what it actually needs.
… block

`INTERPRETING.md` says these cases run only when [[CanBlock]] is false, but the
oracle parsed the flag and ignored it, so they ran on a blocking agent and could
only fail. Carry the flag through the plan into `Limits::agent_can_suspend`, which
keeps its default of true for every other case.
`own_descriptor` fell through to "no such property" for ArrayBuffer,
SharedArrayBuffer, and DataView, so a property set on one was invisible to
`Object.getOwnPropertyDescriptor` and `Object.keys` even though
`hasOwnProperty` and reads found it.

Promise and the generator entries fall through the same way and are not changed
here.
date_until_year_month_day expected 13 days from 2020-02-25 to 2020-03-10.
That cannot hold: the test contradicts its own round-trip assertions,
which require start + d == end, and 2019-02-25 + 1Y + 13D is 2020-03-09.
February 2020 has 29 days, so the distance is 14.

The arithmetic was checked independently of this library with Python's
datetime: date(2020, 3, 10) - date(2020, 2, 25) is 14 days. No
implementation could satisfy the test as written.

This is a deliberate, authorized exception to the rule against changing
expected values. It changes only the two literals. The inputs and the
round-trip assertions are unchanged, because they are the oracle.
These expectations pinned behaviour of code that had never been run
against test262. Each one below is directly contradicted by the named
file in the pinned suite, so the suite wins. This commit changes only
tests; the behaviour that satisfies them follows separately.

This is a deliberate, authorized exception to the rule against changing
expected values, limited to assertions a specific test262 file
contradicts.

duration_parse_fractional_seconds
  old: PT0.5H => (hours, milliseconds) == (0, 1_800_000)
  new: PT0.5H => (hours, minutes, milliseconds) == (0, 30, 0)
  test/built-ins/Temporal/Duration/from/
    argument-string-fractional-units-rounding-mode.js
  PT1.03125H must give 1h 1m 52s 500ms: a fraction spills through
  minutes and seconds.

instant_parse_annotations
  old: Instant::parse("...Z[!u-ca=japanese]").is_err()
  new: Instant::parse("...Z[!u-ca=japanese]").is_ok()
  test/built-ins/Temporal/Instant/from/
    argument-string-calendar-annotation.js
  "critical flag has no effect": [!u-ca=hebrew] and [!u-ca=discord]
  must be accepted, because an Instant has no calendar.

date_parse_extended_years_round_trip
  old: PlainDate::parse("2000-05-15T12:30:00Z") yields 2000-05-15
  new: that string is an error; the date-portion assertion it carried
       now uses "2000-05-15T12:30:00", without the designator
  test/built-ins/Temporal/PlainDate/from/
    argument-string-with-utc-designator.js
  "2019-10-01T09:00:00Z" must throw RangeError.

cross_type_year_month_and_month_day
  old: ym.format() == "2024-02-01"
  new: ym.format() == "2024-02"
year_month_arithmetic_and_difference
  old: later.format() == "2022-01-01"
  new: later.format() == "2022-01"
  old: PlainYearMonth::parse("2020-11").unwrap().format() == "2020-11-01"
  new: PlainYearMonth::parse("2020-11").unwrap().format() == "2020-11"
  test/built-ins/Temporal/PlainYearMonth/prototype/toString/
    calendarname-auto.js
  An ISO year-month serializes as "2000-05", with no reference day.
run_process clears the child environment and forwards only SystemRoot and
windir on Windows. A Windows process finds its temporary directory
through TEMP and TMP, so std::env::temp_dir() in a child was C:\Windows.
The CLI builds its fallback cache root under that directory and failed
with "access denied" before it compiled anything.

Forward TEMP and TMP next to SystemRoot. They stay out of NORMALIZED_ENV,
which evidence records byte for byte. Correct the comment that said no
forwarded variable reaches the guest program.
When a corpus child outlives its timeout the harness kills it and waits.
Unix then reports the signal and no exit code. Windows reports the 1 that
TerminateProcess sets, so a timed-out run looked like a program that
exited with status 1, and a timeout compared differently on the two
platforms.

A child the harness killed has no exit code of its own: record none when
the run timed out. Unix already behaved this way.
The project tests wrote roots such as /workspace and /project, which have
no drive and so are not absolute on Windows. ProjectRoot::new rejected
them, and about a hundred tests failed before they reached their
assertions.

Add a test-only helper, project::test_paths::abs, that returns the path
unchanged where it is already absolute and puts it on the C: drive on
Windows. Fixture roots, inputs and expected paths all go through it, so
no expected value changed. Paths that sit inside JSON text use abs_json,
which escapes the backslashes, and the trace tests derive the drive that
upstream traces print ("c:") instead of writing it out.
The output planning tests used /workspace as the project root. A path
without a drive is not absolute on Windows, so plan_outputs rejected the
root before it reached the assertions. The context test set TMPDIR, which
Windows does not read; it reads TMP and TEMP.

Build the fixture roots with a small helper that keeps the Unix text and
adds the C: drive on Windows, derive the expected paths from it, and set
the variable the platform reads. No expected value changed.
The lane tests spawned /bin/sleep and /usr/bin/true, which Windows does
not have (os error 3), and bound the snapshot to /canonical/ts-suite,
which has no drive and so is not an absolute root there.

Pick the worker per platform: sleep and true on Unix, PING.EXE with three
echoes and cmd.exe /C exit 0 from the system directory on Windows. Build
the snapshot root and the worker directory with a helper that keeps the
Unix text and adds the C: drive on Windows. The assertions are unchanged.
The module doc said every canonical path in the product goes through
canonicalize. Several checks in bamts-verification still call
fs::canonicalize, on purpose: each compares a path only with other
fs::canonicalize results, so the two spellings never meet.

Limit the promise to the compiler, the CLI and the bamts facade, name the
kind of code that may keep the platform call, and state the rule for the
rest: a path compared with compiler output must use this function.
…bute

Windows flushes a file only through a handle with write access, and a
file with the read-only attribute refuses that handle (access denied in
both open modes). sync_tree flushes every extracted file, so an archive
entry that tar unpacked with the attribute made source materialization
fail on Windows. The Unix flush through a read-only handle already
covered the same case, and its test is limited to Unix.

On Windows, when the write handle is refused and the file has the
attribute, lift it for the flush and put it back whether or not the flush
worked. The helper that clears the flag exists only on Windows, where it
touches nothing but the attribute. The Unix code is the same as before.
The comments said a directory cannot be opened for writing on Windows.
That is not true: on NTFS a handle opened with FILE_FLAG_BACKUP_SEMANTICS
for writing accepts a flush (checked on a Windows 11 host). What is true
is that std cannot open a directory there and that this code does not
open one that way.

Say that, and say what the skip costs: NTFS journals the rename, which is
not a promise that the rename is on disk after a power loss. The code is
unchanged.
The first two lines of the GotEntry doc comment described needs_got_entry,
so the alias read as "whether the relocation reads its symbol through a
GOT entry" and the function had no doc at all. Move those lines to the
function. No code changes.
bamts-platform had a Linux and a macOS backend and answered
Unsupported on Windows, so the host-JIT region tests, the peak-RSS
telemetry and the peak-memory window could not run there.

Add a Windows backend with no new dependency. The few Win32 calls it
needs are declared by hand, and each unsafe block states why it is
sound. Struct sizes are asserted at build time.

process_regions walks VirtualQueryEx and reports committed regions as
rwxp strings: copy-on-write counts as writable, guard pages and
PAGE_NOACCESS as ---p. File-backed regions name their file as a DOS path,
mapped from the NT device with QueryDosDeviceW, and UNC shares as \\server.
peak_memory_bytes reports the peak working set, and PeakWindow samples the
working set every millisecond, as the macOS window does.

A process that has exited answers ProcessGone at once, even while its
parent still holds a handle: the host keeps answering reads until the last
handle closes, which would hang a poller that joins before the child is
dropped. A live process that refuses a read gets NoAddressSpace, and a
refusal on a live process for any other reason is a read error.

The pure parts, the permission letters and the NT-to-DOS mapping, are
built on every host so their tests run on Linux and macOS too.
The map sampler and the peak poller tests started /bin/cat and
/bin/sh, which Windows does not have, and the late-mapping test
compiled a POSIX C program with cc. With the Windows platform backend
in place they can run, once they start a child that exists there.

Pick the child per platform. sort.exe from the system directory stays
alive until its input closes, as cat does; ping with two echoes outlives
a few polls, as sleep does. The late-mapping child is a small Rust
program that calls VirtualAlloc and VirtualProtect, built with the rustc
of this toolchain; the Unix C child is unchanged and only moved into a
helper.

The sampler compares the executable with region path names, which are
plain paths, so the controlled AOT child and the tests canonicalize it
with the shared helper instead of fs::canonicalize, whose verbatim
spelling would never match. The assertions are unchanged.
The Windows backend answered only process regions and peak memory. Every
other reader still said Unsupported, so the stage-0 and JIT benchmark
harnesses could not record the conditions of the host they ran on.

machine_facts reads the real Windows version with RtlGetVersion and the
update revision from the registry, the CPU model from the registry, and
sockets and cores per socket from GetLogicalProcessorInformationEx. The
microcode comes from the four-byte Update Revision value, the only form
checked against a host; any other form is UNAVAILABLE rather than a guess.
swap_total_kib sums EnumPageFiles. online_cpu_list and cpu_affinity count
across processor groups. The NUMA nodes come from the same topology
records, and thread_placement formats them like the Linux reader.

A Windows host has no cpufreq governor and no numactl policy, so those
facts are UNAVAILABLE, as on macOS. Every value was compared with CIM and
the registry on the development host. The record parsers and the CPU list
formatting build on every host, so their tests run on Linux and macOS too.
…msvc

The MSVC linker takes no libraries from a Rust staticlib. Linking the
embedded runtime archive needs the system libraries that its crates link
against, kernel32, ntdll, advapi32 and the C runtime among them, and only
rustc knows that list. A fixed list would go stale the next time a
dependency adds a link attribute: the platform crate's registry reader
already added advapi32.

When the archive is assembled for an msvc target, ask rustc for
--print=native-static-libs, parse its note, and export the list as
BAMTS_NODE_NATIVE_LIBS. A missing note on msvc fails the build rather than
yielding an empty list. Every other target runs rustc with the same
arguments as before and exports an empty value.
The AOT path linked with "cc object archive -o executable", which is not an
MSVC command line, so no native executable could be built on Windows.
The emitted object is a valid COFF x64 object (checked with dumpbin: only
REL32 and ADDR64 relocations, the entry and the program descriptor defined,
the runtime helpers undefined), the C main comes from the runtime archive,
and a link.exe run produces an executable whose output matches node's.

Add windows_link, which finds the linker, builds the LIB search path and
builds the command line. The linker is BAMTS_LINKER if set, where a wrong
value is an error and not a reason to look further; else the first link.exe
on PATH that prints the Microsoft banner, which skips the GNU link that Git
for Windows ships; else the newest Visual Studio with the C++ tools, found
with vswhere at its fixed Installer place. LIB comes from the context when
it has one, and otherwise from the toolset of the linker and the newest
complete Windows SDK. The child environment is the context, minus the
LINK and _LINK_ variables that link.exe reads as options. A cleared
environment still works: vswhere gets ProgramData, and the system folders
the context does not name are looked for on the drive of SystemRoot.
Standard output is kept in LinkFailed, because link.exe prints its errors
there. The runtime archive is linked with the libraries from build.rs and
/BREPRO, so equal inputs give equal bytes.

The driver gets a small Linker seam. The Unix code that ran cc moves into
it unchanged, and on msvc the seam is windows_link.
tsc_type_error_emits_with_status_2 looked for the emitted executable at
the source path with its extension removed. The driver names an
executable with the platform suffix, so on Windows the file is
"name.exe" and the test could not find it.

Use std::env::consts::EXE_EXTENSION, which is empty on Unix, so the path
the test checks there is the same as before.
Drop the test-or-Windows gates that repeat the gate of the module, build Read and Malformed errors through read_failed and malformed, name the registry sources once, and share the UTF-16 conversion. No behavior changes.
…ped files

process_regions asked the host for all 26 drive letters on every call. GetLogicalDrives lists the letters in use, so a host with four drives is asked four times. A host that lists none is still asked for every letter.
The build script matched the target name twice. The match on the staticlib build is a statement again, as it was before the native libraries were recorded.
Share the case-insensitive environment lookup with the Windows temporary directory, start the linker probes through one context command, search PATH through one iterator, and keep the runtime library list inside windows_link. The driver takes the program name from the command it runs.
The lane, perf and perf_stage0 tests each built the System32 path by hand.
…ng the toolset

toolset_of took the first four bytes of the directory above the linker. A name such as a Korean directory has a character that spans byte 4, so the slice panicked instead of finding no toolset. Also say in the BAMTS_LINKER doc that a linker outside a Visual Studio layout finds its libraries through LIB.
The walk must end with an error when a whole header names a size below eight bytes, or a size of zero would never advance. The old test cut the header short and never reached that check. A failed wait is now named after the wait call, not after OpenProcess.
…l links on msvc

A named program wins over Visual Studio, a Microsoft linker on PATH wins over Visual Studio, a cancelled token stops each stage of the search, and two links of the same source give the same bytes because of /BREPRO.
…in the compiler worker

load_bound_compiler_snapshot hashed pin, index and ledger (180 MB) twice per case: once to check the lane binding and once for CompilerSnapshot::metadata_digest, which nothing reads. Drop the unread field and its hash. The binding check still authenticates the same bytes. One worker call drops from about 590 ms to about 470 ms.
… from one process

With BAMTS_LANE_WORKER_SERVE set, ts_lane_worker prints a ready line, then answers one request path per line of standard input and prints a done line with the request ID after each response is written. It authenticates the snapshot of a binding once and keeps it for the later requests of that binding. With BAMTS_LANE_WORKER_REQUEST set it still answers the one request, unchanged. A failure that ends a one-request worker ends a serving worker the same way, so the parent can record the same row. The parent does not use this mode yet.
…per shard

Each compiler case used to start ts_lane_worker anew, and each start read, hashed and parsed the 180 MB snapshot metadata before a compile of about 13 ms. A conformance shard of about 16,700 cases needed hours and hit the 45-minute job limit on every CI run.

ServingExecutor keeps one serving worker for many requests. Each request keeps its own request file, response file, binding check in the parent and deadline. A worker that exits or misses a deadline gives its request the observation ProcessExecutor gives such a worker, and the next request starts a new worker. The suite uses it for the compiler runner only; the other lane workers still answer one request per process. ProcessObservation::Answered marks a response from a worker that keeps running and derives the row the same way as a clean exit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant